# sdf2convert.py
#
# FDO Toolbox script to convert MapGuide 6.5 SDF files (SDF 2.0) to any FDO flat-file
# data store.
#
# Author: Jackie Ng (jumpinjackie@gmail.com)
#
# TODO: Utilise MgCoordinateSystem API if available to perform CS lookups.

from System import Activator, Type, Exception, String, Array
from System.IO import Path
from System.Collections.Generic import *
from System.Runtime.InteropServices import COMException
from FdoToolbox.Base.Forms import ProgressDialog
from FdoToolbox.Core.Utility import ExpressUtility
from OSGeo.FDO.Schema import *
from OSGeo.FDO.Expression import *
from OSGeo.FDO.Geometry import *
from OSGeo.FDO.Commands.SpatialContext import SpatialContextExtentType

# FDO Dimensionality Flags
FDO_DIM_XY = 0
FDO_DIM_Z = 1
FDO_DIM_M = 2

# SDF Component Toolkit flags
sdfOpenRead = 1
sdfOpenUpdate = 2
sdfCreateNew = 4
sdfCreateAlways = 8
sdfOpenAlways = 16
sdfOpenExisting = 32

# SDF Object Type flags
sdfUndefinedObject = -1
sdfPointObject = 0
sdfPolylineObject = 2
sdfPolyPolylineObject = 3
sdfPolygonObject = 4
sdfPolyPolygonObject = 5
sdfDeletedObject = 10

# SDF error codes (from SDF Component Toolkit documentation)

errorCodes = Dictionary[str,str]()
#errorCodes["0x00000000"] = "sdfOk"
#errorCodes["0x00041000"] = "sdfEof"
#errorCodes["0x00041000"] = "sdfSearchComplete"
errorCodes["0x8004100F"] = "sdfBadEof"
errorCodes["0x8004100E"] = "sdfBadFormat"
errorCodes["0x80041014"] = "sdfContainsNoObjects"
errorCodes["0x8004100C"] = "sdfElementAlreadyDefined"
errorCodes["0x80041013"] = "sdfIllegalFunctionCall"
errorCodes["0x80041020"] = "sdfInvalidCoordSysCode"
errorCodes["0x8004100B"] = "sdfInvalidElement"
errorCodes["0x80041008"] = "sdfInvalidOffset"
errorCodes["0x80041007"] = "sdfInvalidParameter"
errorCodes["0x8004100A"] = "sdfInvalidPrecision"
errorCodes["0x80041009"] = "sdfInvalidType"
errorCodes["0x80041001"] = "sdfNotAnSdf"
errorCodes["0x80041011"] = "sdfNotFound"
errorCodes["0x80041010"] = "sdfNotSupported"
errorCodes["0x80041019"] = "sdfObjectConstructionFailed"
errorCodes["0x80041015"] = "sdfObjectNotFound"
errorCodes["0x80041018"] = "sdfOutOfBounds"
errorCodes["0x80041012"] = "sdfOutOfMemory"
errorCodes["0x80041016"] = "sdfPolygonUnclosed"
errorCodes["0x80041017"] = "sdfPrecisionMismatch"
errorCodes["0x80041002"] = "sdfReadError"
errorCodes["0x80041005"] = "sdfReadOnly"
errorCodes["0x80041006"] = "sdfSearchNotInitialized"
errorCodes["0x8004100D"] = "sdfTooManyPoints"
errorCodes["0x80041000"] = "sdfUnableToOpen"
errorCodes["0x80041004"] = "sdfVersionMismatch"
errorCodes["0x80041003"] = "sdfWriteError"

#errorCodes["0x00000000"] = "sifOk"
#errorCodes["0x00041000"] = "sifSearchComplete"
#errorCodes["0x00041000"] = "sifTraversalComplete"
errorCodes["0x80042009"] = "sifBadNodeExtent"
errorCodes["0x8004200B"] = "sifBadNodeOffset"
errorCodes["0x8004200A"] = "sifBadObjectCount"
errorCodes["0x8004200D"] = "sifCantCreateTempFile"
errorCodes["0x8004200F"] = "sifCantReplaceSif"
errorCodes["0x80042007"] = "sifInvalidParameter"
errorCodes["0x8004200C"] = "sifNodeUnderflow"
errorCodes["0x80042001"] = "sifNotAnSif"
errorCodes["0x80042008"] = "sifObjectNotFound"
errorCodes["0x80042002"] = "sifReadError"
errorCodes["0x80042005"] = "sifReadOnly"
errorCodes["0x80042006"] = "sifSearchNotInitialized"
errorCodes["0x8004200E"] = "sifTempFileWriteError"
errorCodes["0x80042006"] = "sifTraversalNotInitialized"
errorCodes["0x80042000"] = "sifUnableToOpen"
errorCodes["0x80042004"] = "sifVersionMismatch"
errorCodes["0x80042003"] = "sifWriteError"

#errorCodes["0x00000000"] = "kifOk"
#errorCodes["0x00041000"] = "kifSearchComplete"
errorCodes["0x8004300B"] = "kifBadNodeOffset"
errorCodes["0x8004300A"] = "kifBadObjectCount"
errorCodes["0x8004300D"] = "kifCantCreateTempFile"
errorCodes["0x8004300F"] = "kifCantReplaceKif"
errorCodes["0x80043009"] = "kifDuplicateObject"
errorCodes["0x80043007"] = "kifInvalidParameter"
errorCodes["0x80043011"] = "kifKeysNotOrdered"
errorCodes["0x80043010"] = "kifKeyTooLong"
errorCodes["0x8004300C"] = "kifNodeUnderflow"
errorCodes["0x80043001"] = "kifNotAKif"
errorCodes["0x80043008"] = "kifObjectNotFound"
errorCodes["0x80043002"] = "kifReadError"
errorCodes["0x80043005"] = "kifReadOnly"
errorCodes["0x80043006"] = "kifSearchNotInitialized"
errorCodes["0x8004300E"] = "kifTempFileWriteError"
errorCodes["0x80043000"] = "kifUnableToOpen"
errorCodes["0x80043004"] = "kifVersionMismatch"
errorCodes["0x80043003"] = "kifWriteError"

SDF_TOOLKIT_PROGID = "Autodesk.MgSdfToolkit"
SDF2_SCHEMA = "SDF2Schema"

class Sdf2ConversionException(Exception):
	def __init__(self):
		Exception.__init__(self)
	def __init__(self, message):
		Exception.__init__(self, message)
	def __init__(self, message, innerException):
		Exception.__init__(self, message, innerException)

"""
Creates a COM object from the given prog ID
"""
def CreateObject(progId):
	type = Type.GetTypeFromProgID(progId)
	return Activator.CreateInstance(type)

"""
Gets the error message from a COM Exception based on the HRESULT error code
"""
def GetComErrorMsg(hresult):
	codeStr = String.Format("0x{0:X}", hresult)
	return errorCodes[codeStr] if errorCodes.ContainsKey(codeStr) else ""

"""
Creates a LL84 spatial context
"""
def CreateLL84SpatialContext():
	sc = SpatialContextInfo()
	sc.Name = "Default"
	sc.Description = "Lat-Long WGS84"
	sc.CoordinateSystem = "GEOGCS['LL84',DATUM['WGS84',SPHEROID['WGS84',6378137.000,298.25722293]],PRIMEM['Greenwich',0],UNIT['Degree',0.01745329251994]]"
	sc.CoordinateSystemWkt = sc.CoordinateSystem
	sc.ExtentType = SpatialContextExtentType.SpatialContextExtentType_Dynamic
	sc.IsActive = True
	sc.XYTolerance = 0.001
	sc.ZTolerance = 0.001
	return sc

"""
Creates the SDF2 FDO feature schema
"""
def CreateSdf2Schema(className):
	schema = FeatureSchema(SDF2_SCHEMA, "")
	fc = FeatureClass(className, "")
	
	id = DataPropertyDefinition("Autogenerated_SDF_ID", "")
	name = DataPropertyDefinition("Name", "")
	key = DataPropertyDefinition("Key", "")
	url = DataPropertyDefinition("Url", "")
	geom = GeometryPropertyDefinition("Geom", "")
	
	id.DataType = DataType.DataType_Int32
	name.DataType = DataType.DataType_String
	key.DataType = DataType.DataType_String
	url.DataType = DataType.DataType_String
	
	name.Length = 255
	key.Length = 255
	url.Length = 255
	
	id.Nullable = False
	name.Nullable = True
	key.Nullable = False
	url.Nullable = True
	
	geom.GeometryTypes = GeometricType.GeometricType_All
	geom.SpatialContextAssociation = "Default"
	
	fc.Properties.Add(id)
	fc.Properties.Add(name)
	fc.Properties.Add(key)
	fc.Properties.Add(url)
	fc.Properties.Add(geom)
	
	fc.IdentityProperties.Add(id)
	fc.GeometryProperty = geom
	
	schema.Classes.Add(fc)
	return schema
	
"""
Converts a SdfGeometry to a FGF geometry
"""
def ConvertToFgf(sdfGeometry, sdfGeometryType, geomFactory):
	if sdfGeometry.IsEmpty():
		return None
	if sdfGeometryType == sdfPointObject:
		sdfPoint = sdfGeometry.GetAt(0)
		pos = geomFactory.CreatePositionXY(sdfPoint.X, sdfPoint.Y)
		pt = geomFactory.CreatePoint(pos)
		fgf = geomFactory.GetFgf(pt)
		pt.Dispose()
		pos.Dispose()
		return GeometryValue(fgf)
	elif sdfGeometryType == sdfPolylineObject:
		ordinates = Array[str](range(sdfGeometry.TotalPoints * 2))
		# Should only have one segment
		sdfGeomSeg = sdfGeometry.GetAt(0)
		for i in range(0, sdfGeomSeg.Size - 1):
			sdfPoint = sdfGeomSeg.GetAt(i)
			ordinates[i*2] = sdfPoint.X
			ordinates[i*2+1] = sdfPoint.Y
		linestr = geomFactory.CreateLineString(FDO_DIM_XY, ordinates.Length, ordinates)
		fgf = geomFactory.GetFgf(linestr)
		linestr.Dispose()
		return GeometryValue(fgf)
	elif sdfGeometryType == sdfPolyPolylineObject:
		lineStringCol = LineStringCollection()
		for i in range(0, sdfGeometry.Size - 1):
			ordinates = Array[str](range(sdfGeometry.TotalPoints * 2))
			# Should only have one segment
			sdfGeomSeg = sdfGeometry.GetAt(i)
			for j in range(0, sdfGeomSeg.Size - 1):
				sdfPoint = sdfGeomSeg.GetAt(j)
				ordinates[j*2] = sdfPoint.X
				ordinates[j*2+1] = sdfPoint.Y
			linestr = geomFactory.CreateLineString(FDO_DIM_XY, ordinates.Length, ordinates)
			lineStringCol.Add(linestr)
		multiLine = geomFactory.CreateMultiLineString(lineStringCol)
		fgf = geomFactory.GetFgf(fgf)
		multiLine.Dispose()
		lineStringCol.Dispose()
		return GeometryValue(fgf)
	elif sdfGeometryType == sdfPolygonObject:
		# GUESS: SDF Com Tk docs doesn't really specify how polygons are to be interpreted
		# so, I'm going on the assumption here the first segment is the exterior ring and 
		# every segment after are the interior rings
		innerRings = LinearRingCollection()
		
		# Process outer ring
		ordinates = Array[str](range(sdfGeometry.TotalPoints * 2))
		sdfGeomSeg = sdfGeometry.GetAt(0)
		for j in range(0, sdfGeomSeg.Size - 1):
			sdfPoint = sdfGeomSeg.GetAt(j)
			ordinates[j*2] = sdfPoint.X
			ordinates[j*2+1] = sdfPoint.Y
		outerRing = geomFactory.CreateLinearRing(FDO_DIM_XY, ordinates.Length, ordinates)
		
		# Process interior rings
		for i in range(1, sdfGeometry.Size - 1):
			ordinates = Array[str](range(sdfGeometry.TotalPoints * 2))
			sdfGeomSeg = sdfGeometry.GetAt(i)
			for j in range(0, sdfGeomSeg.Size - 1):
				sdfPoint = sdfGeomSeg.GetAt(j)
				ordinates[j*2] = sdfPoint.X
				ordinates[j*2+1] = sdfPoint.Y
			ring = geomFactory.CreateLinearRing(FDO_DIM_XY, ordinates.Length, ordinates)
			innerRings.Add(linestr)
		
		polygon = geomFactory.CreatePolygon(outerRing, innerRings)
		fgf = geomFactory.GetFgf(fgf)
		polygon.Dispose()
		innerRings.Dispose()
		return GeometryValue(fgf)
	elif sdfGeometryType == sdfPolyPolygonObject:
		# SDF Com TK documentation definitely fails me here! Just how do we know where a 
		# polygon ends and another one starts?
		return None
	else:
		return None

"""
Performs the actual conversion
"""
def Convert(sdfFile, dstFile):
	try:
		gf = FgfGeometryFactory()
		App.WriteLine("Attempting to obtain SDF COM tk interface")
		sdfTk = CreateObject(SDF_TOOLKIT_PROGID)
		if sdfTk is None:
			raise Sdf2ConversionException("Could not obtain the SDF component toolkit interface. Please ensure that the MapGuide SDF Component Toolkit has been installed")
		
		App.WriteLine("Opening for read: " + sdfFile)
		sdfTk.Open(sdfFile, sdfOpenRead, False)
		App.WriteLine("Opened: " + sdfFile)
		if not dstFile is None:
			if ExpressUtility.CreateFlatFileDataSource(dstFile, True):
				App.WriteLine("Created: " + dstFile)
				# all good. Let the conversion begin!
				try:
					total = sdfTk.TotalObjects
					fdoConn = ExpressUtility.CreateFlatFileConnection(dstFile)
					
					# create and apply SDF2 feature schema
					className = Path.GetFileNameWithoutExtension(sdfFile)
					schema = CreateSdf2Schema(className)
					
					svc = fdoConn.CreateFeatureService(False)
					try:
						svc.ApplySchema(schema)
						App.WriteLine("SDF2 Schema applied")
						
						# create spatial context from CS metadata
						csMeta = sdfTk.CoordinateSystemMetadata
						if csMeta.CoordinateSystemCode == "LL84":
							sc = CreateLL84SpatialContext()
							svc.CreateSpatialContext(sc, True)
							App.WriteLine("Spatial Context Created")
						
						ProgressDialog.Show(total, False)
						ProgressDialog.SetTitle("Converting SDF2 features")
						ProgressDialog.SetMessage("Converted:")
						
						count = 0
						failed = 0
						sdfTk.BeginSequentialSearch()
						sdfObj = sdfTk.SearchToNextObject()
						values = Dictionary[str,ValueExpression]()
						while not sdfObj is None:
							geomValue = ConvertToFgf(sdfObj.Geometry, sdfObj.Type)
							if not geomValue is None:
								values.Clear()
								values["Name"] = StringValue(sdfObj.Name)
								values["Key"] = StringValue(sdfObj.Key)
								values["Url"] = StringValue(sdfObj.URL)
								values["Geom"] = geomValue
								svc.InsertFeatures(className, values, False)
								count += 1
								ProgressDialog.SetValue(count)
							else:
								failed += 1
							sdfObj = sdfTk.SearchToNextObject()
						
					finally:
						ProgressDialog.Stop()
						svc.Dispose()
						
					App.ShowMessage("Convert", count.ToString() + " of " + total.ToString() + " features converted. " + failed.ToString() + " features failed to convert")
				except Exception, ex:
					raise Sdf2ConversionException("Error occurred during conversion", ex)
			else:
				raise Sdf2ConversionException("Unable to create data source: " + dstFile)
		else:
			raise Sdf2ConversionException("Target file not specified. Cannot continue")
	finally:
		sdfTk = None
		gf.Dispose()

def Run():
	sdfFile = App.GetFileForOpen("Open SDF File", "SDF 2.0 files (*.sdf)|*.sdf")
	dstFile = App.GetFileForSave("Save As", "SDF 3.0 (*.sdf)|*.sdf|SQLite (*.db)|*.db")
	if not String.IsNullOrEmpty(sdfFile):
		if String.IsNullOrEmpty(dstFile):
			App.ShowMessage("Error", "Target file not specified. Aborting")
		else:
			try:
				Convert(sdfFile, dstFile)
			except COMException, ex:
				msg = GetComErrorMsg(ex.ErrorCode)
				if String.IsNullOrEmpty(msg):
					App.ShowMessage("Error", ex.ToString())
				else:
					App.ShowMessage("Error", "Error Code: " + msg)
			except Sdf2ConversionException, ex:
				App.ShowMessage("Error", ex.ToString())

Run()